FE CI/CD Jenkins 적용기

따듯한 netlify에서 젠킨스로 이동하며..

이전 FE CI/CD는 netlify를 이용하고 있었습니다. netlify는 CI/CD뿐만 아니라 배포부터 CDN,DNS까지 모두 제공해주는 좋은 플랫폼입니다.

하지만 이번에 인프라를 전체적으로 AWS로 통합할 계획이 있었고(전체적인 스택 통일과 개발비관리, 그리고 액세스로그를 보고싶었습니다) CI/CD 는 젠킨스, 전체적인 배포 인프라는 s3를 통한 정적 웹호스팅을 기반으로 clodfront와 route53을 이용하기로 했습니다.

사실 배포 이후 과정은 크게 어렵지 않아 문제가 없었습니다.

다만 젠킨스는 이번에 첫 이용이며 이미 BE서버에 올라간 젠킨스를 통해 배포를 하려다 보니 이해도가 낮아 자잘한 이슈들이 많았습니다. 그 자잘한 이슈들을 정리해보려 합니다.

1. env파일을 어떻게 관리할것인가?

레포에 올라가면 안되는 key값의 관리들은 netlify에서는 environment variables를 통해 관리했습니다. 하지만 젠킨스를 통해 배포가 될 경우 결국 정적인 파일을 s3에 올리는것뿐이기에 env파일이 필요했습니다.

그렇다고 env파일을 레포에 올릴수는 없기에 젠킨스 파이프 라인에서 매개변수를 등록하고 해당 매개변수를 받아 스크립트에서 env 파일을 만드는 방식으로 진행했습니다.

  1. 젠킨스 스트링 파라미터로 매개변수를 등록합니다.

    ex_screenshot

  2. 매개변수를 받아 파이프라인에서 env 파일을 만듭니다.

    // package install && create .env
    stage('yarn install') {
        steps {
            echo 'yarn install start'
            sh '''
            yarn install
            echo 'REACT_APP_EARLY_URI="${REACT_APP_EARLY_URI}"' > .env
            '''
            echo 'yarn install done'
        }

2. 빌드가 멈추지 않고 젠킨스가 뻗어버렸다

파이프라인 작성 후 실질적으로 젠킨스를 이용해 build를 진행해 보는데, 자꾸만 install 이후 build job에서 무한 루프에 빠지기 시작합니다. 이 job때문에 아예 젠킨스로 접속도 되지않는 사태가 발생합니다.

여차저차해서 이유를 알게 됬는데, build 시에 젠킨스의 할당된 메모리가 부족한 메모리 이슈임을 알게됬습니다.

일단 엄청나게 많은 메모리가 필요할거라고 생각하진 않고, 팀원분의 도움을 받아 젠킨스 스왑메모리를 설정했습니다.

1. 젠킨스가 있는 ec2에서 스왑파일을 잡아줍니다.
sudo dd if=/dev/zero of=/swapfile bs=128M count=32

2. 스왑파일에 권한을 바꿔줍니다.
sudo chmod 600 /swapfile

3. 스왑파일 영역을 설정해줍니다.
sudo mkswap /swapfile

4. 스왑공간에 파일을 추가합니다.
sudo swapon /swapfile

5. 편집기를 열어 아래파일을 열어줍니다.
sudo vi /etc/fstab

6. 아래 명령어를 끝에 추가해줍니다.
/swapfile swap swap defaults 0 0

7. 이후 젠킨스 config파일을 열어줍니다.
vi /etc/default/jenkins

8. JVM 용량을 알아서 적어줍니다.
# Allow graphs etc. to work even when an X server is present
JAVA_ARGS = "-Django.awt.headless = true"
JAVA_ARGS = "-Xm1024m" <--해당 명령어를 적어줍니다

9. 이후 젠킨스를 리스타트해줍니다.

build success!!

3. 다시 뻗어버린 젠킨스 왜..

CI/CD를 젠킨스로 변경한 이후 잘만 배포하다가 갑자기 build가 30분이상 진행되면서 또다시 젠킨스가 뻗어버립니다.

젠킨스를 재시작하고 콘솔 아웃풋을 확인했지만 별다른 에러로그가 남지 않았으며, 어느시간부터 어떤과정이 메모리를 크게 잡아먹었는지도 알수가 없었습니다.

다만 에러를 확인하려 리트라이를 눌렀을때 다른 배포가 없었음에도 정상적으로 build가 완료되었습니다. 이에 앞으로의 사후처리를 위해 로그를 좀더 꼼꼼하게 남기도록 변경했습니다.

우선적으로 빌드시 메모리를 많이 사용해 다시 뻗는일이 없게하기 위해 타임아웃을 설정하고, 해당 과정에서 어느시간대에 무슨일이 일어났는지 확인하기 위해 타임스태프 옵션도 추가합니다.

    // 전역 options
    options {
        timestamps()
        timeout(time: 180, unit: "SECONDS")
    }

그리고 react build시에 어떤과정에서 얼만큼의 시간이 소요되는지 확인을 위해 speed-measure-webpack-plugin 을 사용하려 합니다.

다만 저희 회사에 프로젝트는 CRA기반이며 eject는 고려대상이 아니었습니다. 그래서 webpack 오버라이딩을 위해 react-app-rewired 와 customize-cra를 사용했습니다.

우선 2가지 라이브러리를 설치해줍니다.

yarn add react-app-rewired
yarn add customize-cra -D

이후 package.json에서 스크립트를 변경해줍니다. 제 경우는 start와 build만 변경해주었습니다.

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build"
}

그리고 최상위 루트에서 config-overrides.js 파일을 생성하고 다음과 같이 작성합니다.

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()

module.exports = function(config, env) {
  config = smp.wrap({
    ...config,
  })

  return config
}

그런데 해당 에러가 발생합니다.

Error: You forgot to add 'mini-css-extract-plugin' plugin
(i.e. `{ plugins: [new MiniCssExtractPlugin()] }`),
please read https://github.com/webpack-contrib/mini-css-extract-plugin#getting-started

mini-css-extract-plugin을 install하고 플러그인 안에 넣었음에도, 자꾸 해당에러가 발생합니다. 검색을 해보니 mini-css-extract-plugin을 직접 빼서 넣어주면 된다고 합니다. 코드를 다음과 같이 수정합니다.

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = config => {
  // config 플러그인 배열에서 MiniCssExtractPlugin 인스턴스가 있다면 반환합니다.
  const miniCssExtractPluginOrigin = config.plugins.find(
    plugin => plugin instanceof MiniCssExtractPlugin
  )

  // MiniCssExtractPlugin를 제외한 나머지 배열을 반환합니다.
  config.plugins = config.plugins.filter(
    plugin => !(plugin instanceof MiniCssExtractPlugin)
  )

  // smp 옵션을 설정합니다.
  const smp = new SpeedMeasurePlugin({
    outputFormat: 'humanVerbose',
    loaderTopFiles: 10,
  })

  // smp 안에 config 값을 넣습니다.
  const newConfig = smp.wrap({
    ...config,
  })

  newConfig.plugins.push(miniCssExtractPluginOrigin)

  return newConfig
}

이후 build를 해보니 최적화 과정에 대한 log가 기록되는것을 확인했습니다.

 SMP  ⏱
General output time took 12,179 ms

 SMP  ⏱  Plugins
TerserPlugin took 6,677 ms
ESLintWebpackPlugin took 555 ms
CssMinimizerPlugin took 418 ms
...

그런데 build 실행은 잘 되더니 갑자기 start 스크립트를 실행하면 또 처음보는 에러가 발생합니다.

Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
 - configuration.plugins[11] should be one of these:
   object { apply, … } | function
   -> Plugin of type object or instanceof Function.
   Details:
    * configuration.plugins[11] should be an object:
      object { apply, … }
      -> Plugin instance.
    * configuration.plugins[11] should be an instance of function.
      -> Function acting as plugin.

대충 웹팩 플러그인의 잘못된 값이 들어갈 경우 발생할 수 있는 에러라고 합니다.

일단 build는 잘됬으니 해당과정에서 config 값을 conslog.log로 찍어보며 추적을 해본결과, build의 경우 MiniCssExtractPlugin가 플러그인에 존재했습니다.

하지만 start에 경우 없어 miniCssExtractPluginOrigin의 값이 undefind인채로 플러그인 배열안에 들어가 에러를 발생시켰습니다.

마지막 줄만 코드를 다음과 같이 변경해줍니다.

// miniCssExtractPluginOrigin -> undefind일 경우 병합 연산자로 처리
newConfig.plugins.push(miniCssExtractPluginOrigin ?? new MiniCssExtractPlugin())




참고

https://devroach.tistory.com/32


Written by@JeongYeonJae
이것저것 쓰는 개발블로그

ResumeGitHub